// import { startPatch } from "./http";
interface Window {
  __Wab_Listener__?: any;
  __Wab_Legacy__?: any;
  __NativeVar__?: PostMessageAbility;
  __NativeVar2__?: PostMessageAbility;
  __NativeVar3__?: PostMessageAbility;
  Wab: typeof Wabo;
}

type ReplyBody = {
  error?: Error | Object | String;
  data: any;
  replyOnce: boolean; // such as WebSocket will be false.
  replyId?: string;
  [key: string]: any;
};

type PostMessageBody = {
  // type: "function" | "value";
  name?: string; // invoke native function
  preferChannel?: 0 | 1 | 2; //  0 : __NativeVar__ (Legacy), 1 : __NativeVar2__ (WebListener), 2 : __NativeVar3__ (WebChannel )
  replyOnce?: boolean; // such as WebSocket will be false.
  replyId?: string; // callback js function after native function executed
  [key: string]: any;
};

type PromiseConstructorValue<T> = {
  resolve: (value: T | PromiseLike<T>) => void;
  reject: (reason?: any) => void;
};

type PostMessageAbility =
  | {
      postMessage: (data: any) => void;
      removeEventListener?: (event: string, listener: EventListener) => void;
    }
  | MessagePort;

type MessageListener = (event: Event, next: Function, stop: Function) => void;
// @ts-ignore
let __NativeVar__: PostMessageAbility;
// @ts-ignore
let __NativeVar2__: PostMessageAbility;
// @ts-ignore
let __NativeVar3__: PostMessageAbility;
// @ts-ignore
let Wab: Wabo;
// @ts-ignore
class Wabo {
  // Counter of callback ids, randomized to avoid
  // any issues during reloads if a call comes back with
  // an existing callback id from an old session
  private replyIdCount = Math.floor(Math.random() * 134217728);
  private __replys__: Record<string, PromiseConstructorValue<any>> = {};
  private __listeners: Array<MessageListener> = [];

  public get messageListeners() {
    return this.__listeners;
  }

  public addMessageListener(listener: MessageListener) {
    this.__listeners.push(listener);
  }

  public removeMessageListener(listener: MessageListener) {
    const index = this.__listeners.indexOf(listener);
    if (index > -1) {
      this.__listeners.splice(index, 1);
    }
  }
  /**
   * native post message handler.
   * @param event MessageEvent
   * @returns void
   */
  public onMessage(event: MessageEvent) {
    console.log("[Wab] onMessage: ", event, event.data);
    if (event.data == "__setupPort__" && event.ports.length > 0) {
      console.log("[Wab] onMessage: __setupPort__", event);
      __NativeVar3__ = event.ports[0];
      (__NativeVar3__ as MessagePort).addEventListener(
        "message",
        this.onMessage.bind(this)
      );
      // 使用 EventTarget.addEventListener 需要显式调用 start() 来开始, onmessage 已经隐含调用此方法
      (__NativeVar3__ as MessagePort).start();
    } else {
      if (!event.data) {
        console.warn("[Wab] onMessage: event.data is undefined.", event);
        return;
      }
      try {
        const rb: ReplyBody = JSON.parse(event.data);
        this.onReply(rb);
      } catch (error) {
        console.error("[Wab] onMessage: MessageHandler error.", error);
      }
      console.log("[Wab] onMessage: MessageHandler proceed.", event);
      this.peelOff(event)();
    }
  }
  /**
   * 洋葱🧅模型
   * @param event
   * @returns
   */
  private peelOff(event: MessageEvent): Function {
    let that = this;
    return function (next: Function): Promise<any> {
      let index = -1;
      let shouldStop = false;
      const dispatch = (i: number): Promise<void> => {
        if (i <= index || shouldStop)
          return Promise.reject(
            new Error("next() called multiple times or stopped")
          );
        index = i;
        let fn = that.__listeners[i];
        if (i == that.__listeners.length) fn = next();
        if (!fn) return Promise.resolve();
        try {
          const stop = () => {
            shouldStop = true;
            return;
          };
          return Promise.resolve(fn(event, dispatch.bind(null, i + 1), stop));
        } catch (error) {
          return Promise.reject(error);
        }
      };
      return dispatch(0);
    };
  }

  public getValue(options: PostMessageBody): any {
    console.log(
      "[Wab] getValue: ",
      __NativeVar__,
      __NativeVar2__,
      __NativeVar3__
    );
    console.log("[Wab] getValue: ", options);
    options.type = "value";
    options.preferChannel = 0;
    return this.preferChannel(options).postMessage(JSON.stringify(options));
  }

  public getValueAsync(options: PostMessageBody): Promise<any> {
    console.log("[Wab] getValueAsync: ", options);
    options.type = "value";
    if (options.preferChannel == 0) {
      throw "[Wab] getValueAsync: LegacyChannel is not a Promise";
    }
    return this.waitReply(options);
  }

  public async callFunctionAsync(options: PostMessageBody): Promise<any> {
    console.log("[Wab] callFunction: ", options);
    options.type = "function";
    return this.waitReply(options);
  }

  public async callFunction(options: PostMessageBody): any {
    return this.preferChannel(options).postMessage(
          JSON.stringify(options)
        );
  }

  private waitReply(options: PostMessageBody): Promise<any> {
    return new Promise((resolve, reject) => {
      try {
        let replyId = "-1";
        // store the call for later lookup
        this.replyIdCount = this.replyIdCount + 1;
        replyId = String(this.replyIdCount);
        this.__replys__[replyId] = { resolve, reject };
        const callData = {
          replyId,
          ...options,
        };
        const result = this.preferChannel(options).postMessage(
          JSON.stringify(callData)
        );
        // @ts-ignore
        if (!result && options.preferChannel == 0) {
          // @ts-ignore
          resolve(result);
        }
      } catch (error) {
        console.error(error);
        reject(error);
      }
    });
  }

  private onReply(rb: ReplyBody) {
    console.log(
      "[Wab] onReply: ",
      rb,
      rb.replyId,
      rb.replyOnce,
      rb.error,
      rb.data
    );
    if (!rb.replyId) {
      console.warn("[Wab] onReply: no replyId", rb);
      return;
    }
    try {
      const pc = this.__replys__[rb.replyId];
      if (pc) {
        if (!rb.error) {
          pc.resolve(rb.data);
        } else {
          pc.reject(rb.error);
        }
      } else if (rb.error) {
        console.warn(rb.error);
      }
      if (rb.replyOnce == true) {
        delete this.__replys__[rb.replyId];
      }
    } catch (e) {
      console.error(e);
    }
    // always delete to prevent memory leaks
    // overkill but we're not sure what apps will do with this data
    delete rb.data;
    delete rb.error;
  }

  /**
   * Destroys the communication bridge between the web page and the native environment.
   */
  public destroy() {
    window.removeEventListener("message", this.onMessage);
    window.removeEventListener("error", this.onError);
    if (
      __NativeVar2__ &&
      typeof __NativeVar2__.removeEventListener == "function"
    ) {
      __NativeVar2__.removeEventListener(
        "message",
        this.onMessage as EventListener
      );
    }
    if (
      __NativeVar3__ &&
      typeof __NativeVar3__.removeEventListener == "function"
    ) {
      __NativeVar3__.removeEventListener(
        "message",
        this.onMessage as EventListener
      );
    }
  }

  onError(
    event: Event | string,
    source?: string,
    lineno?: number,
    colno?: number,
    error?: Error
  ) {
    Wab.callFunction({
      type: "function",
      name: "onError",
      replyOnce: true,
      error: {
        event: typeof event == "string" ? event : JSON.stringify(event),
        source,
        lineno,
        colno,
        error: JSON.stringify(error),
      },
    });
  }
  // TODO: expose this
  private preferChannel(options?: PostMessageBody): PostMessageAbility {
    let preferChannel = options?.preferChannel ?? 2;
    let channel = this.selectChannel(preferChannel);
    console.log("[Wab] preferChannel: ", channel, preferChannel, options);
    while (!channel && preferChannel > -1) {
      console.warn(
        `[Wab.postNative] preferChannel=${preferChannel} is undefined!`
      );
      preferChannel = preferChannel - 1;
      channel = this.selectChannel(preferChannel as 0 | 1 | 2 | undefined);
    }
    return channel;
  }

  private selectChannel(preferChannel: 0 | 1 | 2 = 2): PostMessageAbility {
    return preferChannel == 0
      ? __NativeVar__
      : preferChannel == 1
      ? __NativeVar2__
      : __NativeVar3__;
  }
}

// *****************************************************************************
try {
  Wab = new Wabo();
  // @ts-ignore
  window.Wab = Wab;
  console.log("[start]", Wab);
  Object.freeze(Wab);
  // @ts-ignore
  if (window.__Wab_Listener__) {
    // @ts-ignore
    __NativeVar2__ = window.__Wab_Listener__;
    // @ts-ignore
  }
  if (window.__Wab_Legacy__) {
    // @ts-ignore
    __NativeVar__ = window.__Wab_Legacy__;
  }
  window.addEventListener("message", Wab.onMessage.bind(Wab), false);
  window.addEventListener("error", Wab.onError.bind(Wab), false);
  // startPatch();
} catch (error) {
  console.error("[Wab] error: " + error);
  throw error;
}
// *****************************************************************************
